/* 
 * originally if-linux.c from dhcpcd - DHCP client daemon
 *
 * Original code Copyright 2006-2008 Roy Marples <roy@marples.name>
 * All rights reserved
 *
 * parts Copyright 2011 Daniel Drown <dan-android@drown.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * netlink.c - kernel api for network interface configuration
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/param.h>

#include <arpa/inet.h>
#include <net/if.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <netinet/ether.h>
#include <netpacket/packet.h>

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "dhcpcd-files/netlink.h"
#include "dhcpcd-files/memory.h"

#define BUFFERLEN 512

/* function: get_netlink
 * reads a netlink message from the kernel
 * fd       - fd to read from
 * flags    - flags to hand recv (ex: MSG_DONTWAIT)
 * callback - function to call on each netlink message. called with two arguments: netlink message and user data
 * data     - user data to hand to callback function
 */
static int get_netlink(int fd, int flags, netlink_callback *callback, void *data) {
  char *buffer = NULL;
  ssize_t bytes;
  struct nlmsghdr *nlm;
  int r = -1;

  buffer = xzalloc(BUFFERLEN);
  for (;;) {
    bytes = recv(fd, buffer, BUFFERLEN, flags);
    if (bytes == -1) {
      if (errno == EAGAIN) {
        r = 0;
        goto exiting;
      }
      if (errno == EINTR)
        continue;
      goto exiting;
    }
    for (nlm = (struct nlmsghdr *)buffer;
        NLMSG_OK(nlm, (size_t)bytes);
        nlm = NLMSG_NEXT(nlm, bytes)) {
      r = callback(nlm, data);
      if (r != 0)
        goto exiting;
    }
  }

exiting:
  free(buffer);
  return r;
}

/* function: err_netlink
 * checks a netlink message for error conditions
 * nlm  - netlink message and header
 * data - ignored
 */
static int err_netlink(struct nlmsghdr *nlm, void *data) {
  struct nlmsgerr *err;
  int l;

  if (nlm->nlmsg_type != NLMSG_ERROR)
    return 0;
  l = nlm->nlmsg_len - sizeof(*nlm);
  if ((size_t)l < sizeof(*err)) {
    errno = EBADMSG;
    return -1;
  }
  err = (struct nlmsgerr *)NLMSG_DATA(nlm);
  if (err->error == 0)
    return l;
  errno = -err->error;
  return -1;
}

/* function: send_netlink_cb
 * sends a netlink message and calls the callback on each message returned
 * hdr           - netlink message and header
 * callback      - function to call on the returned messages
 * callback_data - pointer to hand the callback
 */
int send_netlink_cb(struct nlmsghdr *hdr, netlink_callback *callback, void *callback_data) {
  int fd, r;
  struct sockaddr_nl nl;
  struct iovec iov;
  struct msghdr msg;
  static unsigned int seq;

  if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) == -1)
    return -1;
  memset(&nl, 0, sizeof(nl));
  nl.nl_family = AF_NETLINK;
  if (bind(fd, (struct sockaddr *)&nl, sizeof(nl)) == -1) {
    close(fd);
    return -1;
  }
  memset(&iov, 0, sizeof(iov));
  iov.iov_base = hdr;
  iov.iov_len = hdr->nlmsg_len;
  memset(&msg, 0, sizeof(msg));
  msg.msg_name = &nl;
  msg.msg_namelen = sizeof(nl);
  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
  /* Request a reply */
  hdr->nlmsg_flags |= NLM_F_ACK;
  hdr->nlmsg_seq = ++seq;

  if (sendmsg(fd, &msg, 0) != -1)
    r = get_netlink(fd, 0, callback, callback_data);
  else
    r = -1;
  close(fd);
  return r;
}

/* function: send_netlink
 * sends a netlink message
 * hdr - netlink message and header
 */
static int send_netlink(struct nlmsghdr *hdr) {
  return send_netlink_cb(hdr, &err_netlink, NULL);
}

#define NLMSG_TAIL(nmsg) ((struct rtattr *)(((ptrdiff_t)(nmsg))+NLMSG_ALIGN((nmsg)->nlmsg_len)))

/* function: add_attr_l
 * adds a route attribute to a netlink message
 * n      - netlink header/message
 * maxlen - maximum length of netlink header and message, combined
 * type   - type of route attribute
 * data   - route attribute data
 * alen   - length of route attribute data
 */
static int add_attr_l(struct nlmsghdr *n, unsigned int maxlen, int type, const void *data, int alen) {
  int len = RTA_LENGTH(alen);
  struct rtattr *rta;

  if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) {
    errno = ENOBUFS;
    return -1;
  }

  rta = NLMSG_TAIL(n);
  rta->rta_type = type;
  rta->rta_len = len;
  memcpy(RTA_DATA(rta), data, alen);
  n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len);

  return 0;
}

/* function: add_attr_32
 * same as add_attr_l, but with a 32bit int
 */
static int add_attr_32(struct nlmsghdr *n, unsigned int maxlen, int type, uint32_t data) {
  return add_attr_l(n,maxlen,type,&data,sizeof(data));
}

struct netlink_msg_link {
  struct nlmsghdr hdr;
  struct ifinfomsg ifinfo;
  char buffer[64];
};

/* function: if_link
 * changes a network interface link state/mtu
 * ifname - interface name to change
 * mtu    - new mtu (only on LINK_UP)
 * set_up - LINK_UP or LINK_DOWN
 */
int if_link(const char *ifname, int mtu, int set_up) {
  struct netlink_msg_link *nll;
  int retval = 0;

  nll = xzalloc(sizeof(*nll));
  nll->hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
  nll->hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT;
  nll->hdr.nlmsg_type = RTM_SETLINK;
  if (!(nll->ifinfo.ifi_index = if_nametoindex(ifname))) {
    errno = ENODEV;
    retval = -1;
    goto exiting;
  }
  nll->ifinfo.ifi_change = IFF_UP;
  if(set_up == LINK_UP) {
    nll->ifinfo.ifi_flags = IFF_UP;
    if(add_attr_32(&nll->hdr, sizeof(*nll), IFLA_MTU, mtu) < 0) {
      retval = -1;
      goto exiting;
    }
  }

  if (send_netlink(&nll->hdr) == -1)
    retval = -1;

exiting:
  free(nll);
  return retval;
}

struct netlink_msg_addr {
  struct nlmsghdr hdr;
  struct ifaddrmsg ifa; 
  char buffer[64];
};

/* function: family_size
 * returns the size of the address structure for the given family, or 0 on error
 * family - AF_INET or AF_INET6
 */
ssize_t family_size(int family) {
  if(family == AF_INET) {
    return sizeof(struct in_addr);
  } else if(family == AF_INET6) {
    return sizeof(struct in6_addr);
  } else {
    errno = EINVAL;
    return 0;
  }
}

/* function: if_address
 * adds/removes an IP address to/from an interface
 * ifname    - interface name to change
 * family    - address family (AF_INET, AF_INET6)
 * address   - pointer to a struct in_addr or in6_addr
 * cidr      - bitmask of network (example: 24 for AF_INET 255.255.255.0)
 * broadcast - broadcast address (only for AF_INET, ignored for AF_INET6)
 * add_addr  - ADDRESS_ADD or ADDRESS_DELETE
 */
int if_address(const char *ifname, int family, const void *address, int cidr, const void *broadcast, int add_addr) {
  struct netlink_msg_addr *nlm;
  int retval = 0;
  ssize_t addr_size;

  addr_size = family_size(family);
  if(addr_size == 0) {
    return -1;
  }

  nlm = xzalloc(sizeof(*nlm));
  nlm->hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg));
  nlm->hdr.nlmsg_flags = NLM_F_REQUEST;
  if (add_addr == ADDRESS_ADD) {
    nlm->hdr.nlmsg_flags |= NLM_F_CREATE | NLM_F_REPLACE;
    nlm->hdr.nlmsg_type = RTM_NEWADDR;
  } else
    nlm->hdr.nlmsg_type = RTM_DELADDR;
  if (!(nlm->ifa.ifa_index = if_nametoindex(ifname))) {
    errno = ENODEV;
    retval = -1;
    goto exiting;
  }
  nlm->ifa.ifa_family = family;
  nlm->ifa.ifa_prefixlen = cidr;
  nlm->ifa.ifa_scope = 0;
  if(add_attr_l(&nlm->hdr, sizeof(*nlm), IFA_LOCAL, address, addr_size) < 0) {
    retval = -1;
    goto exiting;
  }
  if(family == AF_INET6) 
    if(add_attr_l(&nlm->hdr, sizeof(*nlm), IFA_ADDRESS, address, addr_size) < 0) {
      retval = -1;
      goto exiting;
    }
  if(add_addr == ADDRESS_ADD && family == AF_INET) // AF_INET6 has no broadcast address
    if(add_attr_l(&nlm->hdr, sizeof(*nlm), IFA_BROADCAST, broadcast, addr_size) < 0) {
      retval = -1;
      goto exiting;
    }


  if (send_netlink(&nlm->hdr) == -1)
    retval = -1;
  
exiting:
  free(nlm);
  return retval;
}

struct netlink_msg_route {
  struct nlmsghdr hdr;
  struct rtmsg rt;
  char buffer[256];
};

// only has rtattrs, and the hdr is only for tracking rtattr length
struct netlink_msg_metrics {
  struct nlmsghdr hdr;
  char buffer[256];
};

/* function: if_route
 * create/replace/delete a route
 * ifname      - name of the outbound interface
 * family      - AF_INET or AF_INET6
 * destination - pointer to a struct in_addr or in6_addr for the destination network
 * cidr        - bitmask of network address (example: 24 for AF_INET 255.255.255.0)
 * gateway     - pointer to a struct in_addr or in6_addr for the gateway to use or NULL for an interface route
 * metric      - route metric (lower is better)
 * mtu         - route-specific mtu or 0 for the interface mtu
 * change_type - ROUTE_DELETE, ROUTE_REPLACE, or ROUTE_CREATE
 */
int if_route(const char *ifname, int family, const void *destination, int cidr, const void *gateway, int metric, int mtu, int change_type) {
  struct netlink_msg_route *nlm;
  struct netlink_msg_metrics *metrics_rta;
  unsigned int ifindex;
  int retval = 0;
  ssize_t addr_size;

  addr_size = family_size(family);
  if(addr_size == 0) {
    return -1;
  }

  if (!(ifindex = if_nametoindex(ifname))) {
    errno = ENODEV;
    return -1;
  }

  nlm = xzalloc(sizeof(*nlm));
  metrics_rta = xzalloc(sizeof(*metrics_rta));

  /* setup netlink header */
  nlm->hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
  nlm->hdr.nlmsg_flags = NLM_F_REQUEST;
  metrics_rta->hdr.nlmsg_len = NLMSG_LENGTH(0);

  if (change_type == ROUTE_REPLACE) {
    nlm->hdr.nlmsg_type = RTM_NEWROUTE;
    nlm->hdr.nlmsg_flags |= NLM_F_REPLACE|NLM_F_CREATE;
  } else if (change_type == ROUTE_CREATE) {
    nlm->hdr.nlmsg_type = RTM_NEWROUTE;
    nlm->hdr.nlmsg_flags |= NLM_F_CREATE;
  } else // ROUTE_DELETE
    nlm->hdr.nlmsg_type = RTM_DELROUTE;

  /* setup rtmsg data */
  nlm->rt.rtm_family = family;
  nlm->rt.rtm_table = RT_TABLE_MAIN;
  nlm->rt.rtm_dst_len = cidr;

  if (change_type == ROUTE_DELETE)
    nlm->rt.rtm_scope = RT_SCOPE_NOWHERE;
  else { // ROUTE_REPLACE / ROUTE_CREATE
    nlm->rt.rtm_protocol = RTPROT_BOOT;
    if(gateway == NULL) {
      nlm->rt.rtm_scope = RT_SCOPE_LINK;
    } else {
      nlm->rt.rtm_scope = RT_SCOPE_UNIVERSE;
    }
    nlm->rt.rtm_type = RTN_UNICAST;
  }

  /* setup rtattr data */
  if(add_attr_l(&nlm->hdr, sizeof(*nlm), RTA_DST, destination, addr_size) < 0) {
    retval = -1;
    goto exiting;
  }

  if (gateway != NULL) 
    if(add_attr_l(&nlm->hdr, sizeof(*nlm), RTA_GATEWAY, gateway, addr_size) < 0) {
      retval = -1;
      goto exiting;
    }

  if(add_attr_32(&nlm->hdr, sizeof(*nlm), RTA_OIF, ifindex) < 0) {
    retval = -1;
    goto exiting;
  }
  if(add_attr_32(&nlm->hdr, sizeof(*nlm), RTA_PRIORITY, metric) < 0) {
    retval = -1;
    goto exiting;
  }
  if(mtu > 0 && change_type != ROUTE_DELETE) {
    // first, add mtu to the metrics rtattr
    if(add_attr_32(&metrics_rta->hdr, sizeof(*metrics_rta), RTAX_MTU, mtu) < 0) {
      retval = -1;
      goto exiting;
    }
    // then, add the metrics rtattr(s) to the nl message
    if(add_attr_l(&nlm->hdr, sizeof(*nlm), RTA_METRICS, NLMSG_DATA(&metrics_rta->hdr), metrics_rta->hdr.nlmsg_len - sizeof(metrics_rta->hdr)) < 0) {
      retval = -1;
      goto exiting;
    }
  }

  /* done, send it to the kernel */
  if (send_netlink(&nlm->hdr) == -1)
    retval = -1;

exiting:
  free(nlm);
  free(metrics_rta);
  return retval;
}
